package com.common.sdk.net.connect.http;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;

import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;

import com.android.sohu.sdk.common.toolbox.StringUtils;
import com.common.sdk.net.connect.cache.Cache;
import com.common.sdk.net.connect.http.error.AuthFailureError;
import com.common.sdk.net.connect.http.util.VolleyLog;
import com.common.sdk.net.connect.http.util.VolleyLog.MarkerLog;
import com.common.sdk.net.connect.interfaces.IRequestListener;
import com.common.sdk.net.connect.http.center.tools.HttpLog;

public class DaylilyRequest {
    public static final String ENCODING = "UTF-8";

    private String url;
    private String redirectUrl;
    private int method;

    private ConcurrentHashMap<String, String> queryParams;
    private ConcurrentHashMap<String, String> headerParams;
    protected ConcurrentHashMap<String, String> entityStringParams;

    private transient HttpEntity entity;

    private Object tag;

    private boolean canceled = false;
    
    /** The retry policy for this request. */
    private RetryPolicy mRetryPolicy;

    /**
     * Threshold at which we should log the request (even when debug logging is
     * not enabled).
     */
    private static final long SLOW_REQUEST_THRESHOLD_MS = 3000;

    // A cheap variant of request tracing used to dump slow requests.
    private long mRequestBirthTime = 0;

    /** An event log tracing the lifetime of this request; for debugging. */
    private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;

    /**
     * When a request can be retrieved from cache but must be refreshed from the
     * network, the cache entry will be stored here so that in the event of a
     * "Not Modified" response, we can be sure it hasn't been evicted from
     * cache.
     */
    private Cache.Entry mCacheEntry = null;

    private IRequestListener requestListener;

    private String cachePath;

    private String contentType;

    /**
     * Supported request methods.
     */
    public interface Method {
        int GET = 0;
        int POST = 1;
        int PUT = 2;
        int DELETE = 3;
    }

    public DaylilyRequest(String url, int method) {
        if (url != null) {
            this.url = url.trim();
        } else {
            this.url = "";
        }
        this.method = method;
    }

    public void setUrl(String url) {
        this.url = url;
    }
    
    public String getRedirectUrl() {
		return redirectUrl;
	}

	public void setRedirectUrl(String redirectUrl) {
		this.redirectUrl = redirectUrl;
	}

	public boolean isFileDownload() {
        return false;
    }

    public boolean isCanceled() {
        return canceled;
    }

    public void setCanceled() {
        this.canceled = true;
    }

    public int getMethod() {
        return method;
    }

    public Object getTag() {
        return tag;
    }

    public void setTag(Object tag) {
        this.tag = tag;
    }

    public void setEntity(HttpEntity entity) {
        this.entity = entity;
    }

    public ConcurrentHashMap<String, String> getHeaderParams() {
        return headerParams;
    }

    public void addQueryParam(String key, long value) {
        String stringValue = String.valueOf(value);
        addQueryParam(key, stringValue);
    }

    public void addQueryParam(String key, float value) {
        String stringValue = String.valueOf(value);
        addQueryParam(key, stringValue);
    }

    public void addQueryParam(String key, double value) {
        String stringValue = String.valueOf(value);
        addQueryParam(key, stringValue);
    }

    public void addQueryParam(String key, int value) {
        String stringValue = String.valueOf(value);
        addQueryParam(key, stringValue);
    }

    public void addQueryParam(String key, boolean value) {
        String stringValue = String.valueOf(value);
        addQueryParam(key, stringValue);
    }

    public void addQueryParam(String key, String value) {
        if (queryParams == null) {
            queryParams = new ConcurrentHashMap<String, String>();
        }

        queryParams.put(key, TextUtils.isEmpty(value) ? "" : value);
    }

    public void clearQueryParams() {
        if (queryParams == null) {
            return;
        }

        queryParams.clear();
        queryParams = null;
    }

    public void addHeaderParam(String key, long value) {
        String stringValue = String.valueOf(value);
        addHeaderParam(key, stringValue);
    }

    public void addHeaderParam(String key, float value) {
        String stringValue = String.valueOf(value);
        addHeaderParam(key, stringValue);
    }

    public void addHeaderParam(String key, double value) {
        String stringValue = String.valueOf(value);
        addHeaderParam(key, stringValue);
    }

    public void addHeaderParam(String key, int value) {
        String stringValue = String.valueOf(value);
        addHeaderParam(key, stringValue);
    }

    public void addHeaderParam(String key, boolean value) {
        String stringValue = String.valueOf(value);
        addHeaderParam(key, stringValue);
    }

    public void addHeaderParam(String key, String value) {
        if (headerParams == null) {
            headerParams = new ConcurrentHashMap<String, String>();
        }

        headerParams.put(key, TextUtils.isEmpty(value) ? "" : value);
    }

    public void clearHeaderParams() {
        if (headerParams == null) {
            return;
        }

        headerParams.clear();
        headerParams = null;
    }

    public DaylilyRequest setRequestListener(IRequestListener requestListener) {
        this.requestListener = requestListener;
        return this;
    }

    public void addEntityStringParam(String key, String value) {
        if (entityStringParams == null) {
            entityStringParams = new ConcurrentHashMap<String, String>();
        }

        entityStringParams.put(key, TextUtils.isEmpty(value) ? "" : value);
    }

    public void addEntityStringParam(String key, long value) {
        String stringValue = String.valueOf(value);
        addEntityStringParam(key, stringValue);
    }

    public void addEntityStringParam(String key, float value) {
        String stringValue = String.valueOf(value);
        addEntityStringParam(key, stringValue);
    }

    public void addEntityStringParam(String key, double value) {
        String stringValue = String.valueOf(value);
        addEntityStringParam(key, stringValue);
    }

    public void addEntityStringParam(String key, int value) {
        String stringValue = String.valueOf(value);
        addEntityStringParam(key, stringValue);
    }

    public void addEntityStringParam(String key, boolean value) {
        String stringValue = String.valueOf(value);
        addEntityStringParam(key, stringValue);
    }

    private List<BasicNameValuePair> getQueryParamsList() {
        List<BasicNameValuePair> lparams = new LinkedList<BasicNameValuePair>();

        for (ConcurrentHashMap.Entry<String, String> entry : queryParams.entrySet()) {
            lparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
        }
        return lparams;
    }

    public String getUrlWithQueryString() {
    	if (StringUtils.isNotBlank(redirectUrl)) {
            return redirectUrl;
    	}
        String tmpUrl = this.url;
        if (queryParams != null) {
            String paramString = URLEncodedUtils.format(getQueryParamsList(), ENCODING);
            if (tmpUrl.indexOf("?") == -1) {
                tmpUrl += "?" + paramString;
            } else {
                if (tmpUrl.endsWith("&")) {
                    tmpUrl += paramString;
                } else {
                    tmpUrl += "&" + paramString;
                }
            }
        }

        return tmpUrl;
    }

    protected BasicHeader[] getHeadersWithParams() {
        if (headerParams == null || headerParams.isEmpty()) {
            return null;
        }

        BasicHeader[] headers = new BasicHeader[headerParams.size()];

        int i = 0;
        for (ConcurrentHashMap.Entry<String, String> entry : headerParams.entrySet()) {
            headers[i] = new BasicHeader(entry.getKey(), entry.getValue());
            i++;
        }
        return headers;
    }

    protected HttpEntity getEntityWithParams() {
        if ((entityStringParams == null || entityStringParams.isEmpty()) && entity == null) {
            return null;
        }

        if (entity == null) {
            List<NameValuePair> nvps = new ArrayList<NameValuePair>();
            for (ConcurrentHashMap.Entry<String, String> entry : entityStringParams.entrySet()) {
                nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
            UrlEncodedFormEntity formEntity = null;
            try {
                formEntity = new UrlEncodedFormEntity(nvps, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                // ignore
                HttpLog.error(e);
            }
            return formEntity;
        } else {
            return entity;
        }
    }

    protected String toShortString() {
        String res = "<url>:" + url;
        return res;
    }

    /**
     * Returns which encoding should be used when converting POST or PUT
     * parameters returned by {@link #getParams()} into a raw POST or PUT body.
     * <p>
     * This controls both encodings:
     * <ol>
     * <li>The string encoding used when converting parameter names and values
     * into bytes prior to URL encoding them.</li>
     * <li>The string encoding used when converting the URL encoded parameters
     * into a raw byte array.</li>
     * </ol>
     */
    protected String getParamsEncoding() {
        return ENCODING;
    }

    /**
     * 设置body的内容类型如：application/x-www-form-urlencoded; application/json;等
     * @param contentType
     */
    public void setBodyContentType(String contentType) {
        this.contentType = contentType;
    }

    public String getBodyContentType() {
        if (StringUtils.isBlank(contentType)) {
            return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
        } else {
            return contentType + " charset=" + getParamsEncoding();
        }
    }

    /**
     * Returns the socket timeout in milliseconds per retry attempt. (This value
     * can be changed per retry attempt if a backoff is specified via
     * backoffTimeout()). If there are no retry attempts remaining, this will
     * cause delivery of a {@link TimeoutError} error.
     */
    public final int getCurrentTimeoutMs() {
        return getRetryPolicy().getCurrentTimeout();
    }

    public final int getReadTimeoutMs() {
        return getRetryPolicy().getReadTimeout();
    }

    /**
     * Returns the retry policy that should be used for this request.
     */
    public RetryPolicy getRetryPolicy() {
        if (mRetryPolicy == null) {
            mRetryPolicy = new DefaultRetryPolicy();
        }
        return mRetryPolicy;
    }

    /**
     * Sets the retry policy for this request.
     * 
     * @return This Request object to allow for chaining.
     */
    public DaylilyRequest setRetryPolicy(RetryPolicy retryPolicy) {
        mRetryPolicy = retryPolicy;
        return this;
    }

    /**
     * Returns the raw POST or PUT body to be sent.
     * 
     * @throws AuthFailureError in the event of auth failure
     */
    public byte[] getBody() throws AuthFailureError {
        Map<String, String> params = getParams();
        if (params != null && params.size() > 0) {
            return encodeParameters(params, getParamsEncoding());
        }
        return null;
    }

    /**
     * Returns a Map of parameters to be used for a POST or PUT request. Can
     * throw {@link AuthFailureError} as authentication may be required to
     * provide these values.
     * <p>
     * Note that you can directly override {@link #getBody()} for custom data.
     * </p>
     * 
     * @throws AuthFailureError in the event of auth failure
     */
    public Map<String, String> getParams() {
        return entityStringParams;
    }

    /**
     * Converts <code>params</code> into an application/x-www-form-urlencoded
     * encoded string.
     */
    private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
        StringBuilder encodedParams = new StringBuilder();
        try {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
                encodedParams.append('=');
                encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
                encodedParams.append('&');
            }
            return encodedParams.toString().getBytes(paramsEncoding);
        } catch (UnsupportedEncodingException uee) {
            throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
        }
    }

    /**
     * Adds an event to this request's event log; for debugging.
     */
    public void addMarker(String tag) {
        if (MarkerLog.ENABLED) {
            mEventLog.add(tag, Thread.currentThread().getId());
        } else if (mRequestBirthTime == 0) {
            mRequestBirthTime = SystemClock.elapsedRealtime();
        }
    }

    /**
     * Annotates this request with an entry retrieved for it from cache. Used
     * for cache coherency support.
     * 
     * @return This Request object to allow for chaining.
     */
    public DaylilyRequest setCacheEntry(Cache.Entry entry) {
        mCacheEntry = entry;
        return this;
    }

    /**
     * Returns the annotated cache entry, or null if there isn't one.
     */
    public Cache.Entry getCacheEntry() {
        return mCacheEntry;
    }

    /**
     * Notifies the request queue that this request has finished (successfully
     * or with error).
     * <p>
     * Also dumps all events from this request's event log; for debugging.
     * </p>
     */
    void finish(final String tag) {
        if (requestListener != null) {
            requestListener.finish(this);
        }
        if (MarkerLog.ENABLED) {
            final long threadId = Thread.currentThread().getId();
            if (Looper.myLooper() != Looper.getMainLooper()) {
                // If we finish marking off of the main thread, we need to
                // actually do it on the main thread to ensure correct ordering.
                Handler mainThread = new Handler(Looper.getMainLooper());
                mainThread.post(new Runnable() {
                    @Override
                    public void run() {
                        mEventLog.add(tag, threadId);
                        mEventLog.finish(this.toString());
                    }
                });
                return;
            }

            mEventLog.add(tag, threadId);
            mEventLog.finish(this.toString());
        } else {
            long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime;
            if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) {
                VolleyLog.d("%d ms: %s", requestTime, this.toString());
            }
        }
    }

    public String getCacheKey() {
        return getUrlWithQueryString();
    }

    public String getCachePath() {
        return cachePath;
    }

    public void setCachePath(String cachePath) {
        this.cachePath = cachePath;
    }

    public String getUrl() {
        return url;
    }

}
